package contegor.scire.structure.plugins;

import static contegor.scire.structure.Constants.ACTION;
import static contegor.scire.structure.Constants.TO_CAN;
import static contegor.scire.structure.Constants.LANG_DIRECT_OBJECT;
import static contegor.scire.structure.Constants.FIRST;
import static contegor.scire.structure.Constants.LANG_ENTITY;
import static contegor.scire.structure.Constants.LANG_MEANING_GROUP;
import static contegor.scire.structure.Constants.LANG_NUMBER;
import static contegor.scire.structure.Constants.LANG_PERSON;
import static contegor.scire.structure.Constants.MAIN;
import static contegor.scire.structure.Constants.LANG_NEXUS;
import static contegor.scire.structure.Constants.PARAMETER;
import static contegor.scire.structure.Constants.TO_REQUIRE;
import static contegor.scire.structure.Constants.LANG_SUBJECT;
import static contegor.scire.structure.Constants.UNDEFINED;


import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;

import contegor.scire.core.Action;
import contegor.scire.core.Concept;
import contegor.scire.core.Connectable;
import contegor.scire.core.Id;
import contegor.scire.core.Link;
import contegor.scire.core.Link.Flow;
import contegor.scire.core.MissingElementException;
import contegor.scire.core.Primitive;
import contegor.scire.core.queries.And;
import contegor.scire.core.queries.Condition;
import contegor.scire.core.queries.Filter;
import contegor.scire.core.queries.GetConceptsIn;
import contegor.scire.core.queries.GetLinksFromSource;
import contegor.scire.core.queries.IsConnectedWith;
import contegor.scire.core.queries.Not;
import contegor.scire.core.queries.Or;
import contegor.scire.core.queries.WithFlow;
import contegor.scire.structure.Constants;
import contegor.scire.structure.Context;
import contegor.scire.structure.lang.ActionExecutor;
import contegor.scire.structure.lang.MeaningGroupData;
import contegor.scire.structure.lang.UnderstandException;
import contegor.scire.structure.queries.GetInstancesOf;

/**
 * Action methods for Spanish language.
 * 
 * @author Héctor Cirbián Pousa
 */
public class Es {

  private static class FilledRequirement {
    Link requirement;
    List<Concept> compliments;

    public FilledRequirement(Link requirement, List<Concept> compliments) {
      this.requirement = requirement;
      this.compliments = compliments;
    }
  }

  /**
   * Spanish processor. Given the list of {@link Primitive} that define the sentence meaning groups,
   * it interprets them by Spanish language rules.
   * 
   * An example of meaning group, can be:
   * 
   *  ARTICLE
   *     ^
   *     |---> FIRST
   *     |
   *     |  ENTITY  SUBJECT  lang.concepts.person
   *     |   ^         ^        /
   *     |  /          |    ------> FIRST_PERSON             <---- TODO: ESTO CAMBIA
   *     | /           |   /
   *     |/            |  /    /-----> SINGULAR
   *  G1 o------------------------> LANG_MEANING_GROUP
   *               |        \
   *             BE_SER      --> UNDEFINED
   *
   */
  @SuppressWarnings("unchecked")
  public static void process(Context ctx, Action action, Id groupsContainerId, List<Primitive> groups) throws InterruptedException, ExecutionException, UnderstandException, MissingElementException, SecurityException, IllegalArgumentException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    // Correlate left groups ....
    List<Primitive> groupsLeft = new ArrayList<Primitive>(groups);
    // Understanding must start with the verb ...
    List<Connectable> verbs = ctx.process(new GetInstancesOf(ACTION)).get();
    if ((verbs == null) || (verbs.size() < 1))
      throw new UnderstandException("I can't undesrtand sentences with no verbs.");
    if (verbs.size() > 1)
      throw new UnderstandException("I can't undesrtand sentences with more than one verb.");
    Action verb = (Action)verbs.get(0);
    Primitive verbGroup = (Primitive)ctx.process(new Filter<Concept>(new IsConnectedWith<Concept>(verb.getId()), new GetConceptsIn(groupsContainerId))).get().get(0);
    if (!groupsLeft.remove(verbGroup))
      throw new UnderstandException("Incoherent context with " + verbGroup.getId());

    // Resolve verb requirements ...    
    List<Link> requirements = new ArrayList<Link>(ctx.process(
        new Filter<Link>(
            new Not<Link>(new IsConnectedWith<Link>(PARAMETER)),
            new Filter<Link>(new IsConnectedWith<Link>(TO_REQUIRE), new GetLinksFromSource(verb.getId())))).get());
    // TODO: Get not only requirements but also suggestions.
    List<FilledRequirement> filledRequirements = new ArrayList<Es.FilledRequirement>(requirements.size());
    for (Link requirement: requirements) {
      List<Concept> compliments = ctx.process(new Filter<Concept>(new IsConnectedWith<Concept>(LANG_MEANING_GROUP, new IsConnectedWith<Link>(requirement.getTarget(), WithFlow.DOWNSTREAM)), new GetConceptsIn(groupsContainerId))).get();
      if ((compliments == null) || (compliments.size() == 0)) {
        // Requirement not found ... It can be ...
        if (LANG_SUBJECT.equals(requirement.getTarget())) {
          // Subject and Verb must match person and number;
          Id person = null;
          Id number = null;
          for (Link link: verbGroup.getLink(LANG_MEANING_GROUP).getLinks().values()) {
            if (link.isLinkedWith(LANG_PERSON))
              person = link.getTarget();
            else if (link.isLinkedWith(LANG_NUMBER))
              number = link.getTarget();
          }
          if (person == null)
            throw new UnderstandException("Verb " + verb.getId() + " who's group has not person defined.");
          if (number == null)
            throw new UnderstandException("Verb " + verb.getId() + " who's group has not number defined.");      
          List<Concept> concordances = ctx.process(new Filter<Concept>(
              new IsConnectedWith<Concept>(LANG_MEANING_GROUP, 
                  new And<Link>(WithFlow.DOWNSTREAM, 
                      new IsConnectedWith<Link>(person, WithFlow.DOWNSTREAM),
                      new IsConnectedWith<Link>(number, WithFlow.DOWNSTREAM))), new GetConceptsIn(groupsContainerId))).get();
          if (concordances.size() < 0) {
            // TODO: Try to find it in context.
            throw new UnderstandException("It was not possible to find subject.");
          }
          if (concordances.size() > 1) {
            // Subject usually is the first in the sentence so we start the search from the beginning ... we can't do much more.
            for (Primitive group: groups) {
              if (concordances.contains(group) && groupsLeft.contains(group)) {
                compliments = Arrays.asList(new Concept[] {group});
                break;
              }
            }
          }
          else {
            // Subject found.
            compliments = concordances;
          }
        }
        // ... or a ....
        else if (LANG_DIRECT_OBJECT.equals(requirement.getTarget())) {
          // We will pick the first left group with undefined lang entity starting from the end ... we can't do much more.
          List<Concept> undefined = ctx.process(new Filter<Concept>(
              new IsConnectedWith<Concept>(LANG_MEANING_GROUP, new And<>(new IsConnectedWith<Link>(LANG_ENTITY), new IsConnectedWith<Link>(UNDEFINED))), 
              new GetConceptsIn(groupsContainerId))).get();
          for (int i=groups.size()-1; i>=0; i--) {
            Primitive group = groups.get(i);
            if (group == verbGroup) // Verb line crossed. We won't search further.
              throw new UnderstandException("We can't find direct object for: " + verb.getId());
            if (undefined.contains(group) && groupsLeft.contains(group)) {
              compliments = Arrays.asList(new Concept[] {group});
              break;
            }
          }
        }
        // ... requirement definitely not found.
        else {
          // TODO: Try to find it in context.
          throw new UnderstandException("It was not possible to find requirement: " + requirement.getTarget());
        }
        if ((compliments == null) || (compliments.size() == 0))
          throw new UnderstandException("We can't find requirement " +requirement.getTarget() + " for " + verb.getId());
        // Required group found. Store so it's linked to the action after.
        for (Concept compliment: compliments)
          if (!groupsLeft.remove(compliment))
            throw new UnderstandException("Incoherent context with " + compliment.getId());
        filledRequirements.add(new FilledRequirement(requirement, compliments));
      }
    }

    List<Nearness> possibleFusions = null;
    if (groupsLeft.size() > 0) {
      // Fusion incomplete groups with those that complement them to generates full groups.
      // For example, 'Busca las fotos de las últimas vacaciones' will result in only two groups:
      // verb and direct object, and the last one will have included in its query 'de las últimas vacaciones'.
      possibleFusions = new ArrayList<Nearness>();
      for (Primitive current: groupsLeft) {
        // If first group element is a nexus, try to do the fusion ...
        Link first = null;
        Link main  = null;
        for (Link link: current.getLinks().values()) {
          if (link.isLinkedWith(FIRST))
            first = link;
          if (link.isLinkedWith(MAIN))
            main = link;
        }
        if (LANG_NEXUS.equals(first.getTarget())) {
          // First element is a nexus, so perhaps this group complements a previous one ...
          Nearness nearest = null; 
          List<Concept> currentMeaningChain = Core.extractMeaningChain(ctx, main.getTarget());
          // ... an already selected ...
          for (FilledRequirement filledRequirement: filledRequirements)
            for (Concept toComplete: filledRequirement.compliments)
              nearest = calculateAndSelectMinNearness(ctx, current, first, nearest, currentMeaningChain, toComplete);
          // ... or one not selected yet ...
          for (Primitive toComplete: groupsLeft)
            if (toComplete != current)
              nearest = calculateAndSelectMinNearness(ctx, current, first, nearest, currentMeaningChain, toComplete);
          if (nearest != null)
            possibleFusions.add(nearest);
        }
      }
    }

    // Do the pending fussions ...
    if (possibleFusions != null) {
      // Get fusions sequence and perform them.
      Deque<Nearness> sequence;
      while (possibleFusions.size() > 0) {
        Iterator<Nearness> iterator = possibleFusions.iterator();
        Nearness current = iterator.next();
        sequence = new LinkedList<Nearness>();
        sequence.add(current);
        iterator.remove();
        while (iterator.hasNext()) {
          Nearness sibling = iterator.next();
          if (current.incomplete == sibling.complement) {
            sequence.addFirst(sibling);
            iterator.remove();
          }
          else if (current.complement == sibling.incomplete) {
            sequence.addLast(sibling);
            iterator.remove();
          }
        }
        // Perform fusion that is in sequence.
        // [busca] [las fotos] [de la casa] [del pueblo]
        // Filter(IsConnectedWith(IsConnected('pertenecer'), 
        //    Filter(IsConnectedWith(IsConnected('pertenecer'), GetInstancesOf('pueblo')), GetInstancesOf('casa'))),
        //    GetInstancesOf('fotos'))
        Iterator<Nearness> desc = sequence.descendingIterator();
        while (desc.hasNext()) {
          current = desc.next();
          MeaningGroupData idata = (MeaningGroupData)current.incomplete.getValue();
          MeaningGroupData cdata = (MeaningGroupData)current.complement.getValue();
          List<Concept> clist = cdata.retreive(ctx);
          Condition<Concept> cond;
          if (clist.size() == 1)
            cond = new IsConnectedWith<Concept>(clist.get(0).getId(), new IsConnectedWith<Link>(current.relation));
          else {
            List<Condition<Concept>> conditions = new ArrayList<Condition<Concept>>(clist.size());
            for (Concept concept: clist)
              conditions.add(new IsConnectedWith<Concept>(concept.getId(), new IsConnectedWith<Link>(current.relation)));
            cond = new Or<Concept>(conditions);
          }
          idata.setQuery(new Filter<Concept>(cond, idata.getQueryFilteredWithAttribute()));
        }
      }
    }

    // Link action with it's requirements:
    //
    //     base.core.parameter
    //          ^
    //          |--> obj1, obj2, obj3 ...
    // action ------- direct object
    //
    for (FilledRequirement filledRequirement: filledRequirements) {
      for (Concept compliment: filledRequirement.compliments) {
        // Bring concepts to which the group refers, and create transient links so they are parameters of the action.
        MeaningGroupData data = (MeaningGroupData)((Primitive)compliment).getValue();
        List<Concept> parameters = data.retreive(ctx); // data.setAgainstContext allows to know if the search must be done in the ctx or the cloud.
        ctx.process(ctx.newCreateTransientLink(Flow.DOWNSTREAM, Constants.LANG_ACTIONS, Constants.PARAMETER, verb.getId().getLastIdFragment(), filledRequirement.requirement.getTarget())).get();
        if (parameters != null)
          for (Concept parameter: parameters)
            ctx.process(ctx.newCreateTransientLink(Flow.DOWNSTREAM, Constants.LANG_ACTIONS, parameter.getId(), verb.getId().getLastIdFragment(), filledRequirement.requirement.getTarget(), Constants.PARAMETER)).get();      
      }
    }

    // TODO: Problems with time, place complements and mode adverbs & co. For example:
    // [El último verano] (nosotros) [fuimos] [de vacaciones] [a la playa]
    // [El último verano] = time.
    // [de vacaciones] = mode.

    ActionExecutor executor = new ActionExecutor(ctx);
    executor.execute(verb.getId());
  }

  private static class Nearness {
    int value;
    Id relation;
    Primitive incomplete;
    Primitive complement;

    public Nearness(int value, Id relation) {
      this.value = value;
      this.relation = relation;
      this.incomplete = null;
      this.complement = null;
    }
  }

  private static Nearness calculateAndSelectMinNearness(Context ctx, Primitive current, Link first, Nearness nearest, List<Concept> currentMeaningChain, Concept toComplete) throws InterruptedException, ExecutionException {
    Link prevMain = null;
    for (Link link: toComplete.getLinks().values()) {
      if (link.isLinkedWith(MAIN))
        prevMain = link;
    }
    List<Concept> prevMeaningChain = Core.extractMeaningChain(ctx, prevMain.getTarget());
    Nearness aux = calculateNearness(prevMeaningChain, first, currentMeaningChain);
    if (((aux != null) && (aux.value > 0)) && ((nearest == null) || (nearest.value > aux.value))) {
      nearest = aux;
      nearest.incomplete = (Primitive)toComplete;
      nearest.complement = current;
    }
    return nearest;
  }

  private static Nearness calculateNearness(List<Concept> meaningChain, Link nexusLink, List<Concept> complementMeaningChain) {
    int x = 0;
    for (Concept meaning: meaningChain) {
      x++;
      int y = 0;
      for (Concept complementMeaning: complementMeaningChain) {
        y++;
        // There is a link from meaning to complement.
        Link meaningXcomplement = meaning.getLink(complementMeaning.getId());
        if ((meaningXcomplement != null) && (Flow.DOWNSTREAM.equals(meaningXcomplement.getFlow()))) {
          // It's a CAN link.
          Link linkXcan = meaningXcomplement.getLink(TO_CAN);
          if ((linkXcan != null) && (Flow.NONE.equals(linkXcan.getFlow()))) {
            for (Link link: meaningXcomplement.getLinks().values()) {
              if (Flow.DOWNSTREAM.equals(link.getFlow()) && nexusLink.isLinkedWith(link.getTarget())) {
                return new Nearness(x+y, link.getTarget());
              }
            }
          }
        }
      }
    }
    return null;
  }
}